package com.zm.openapi.filters;

import com.alibaba.fastjson.JSON;
import com.zm.openapi.exception.BusinessException;
import com.zm.openapi.enums.CommonEnum;
import com.zm.openapi.bo.AppSecretBo;
import com.zm.openapi.constants.FilterSeq;
import com.zm.openapi.constants.OpenApiConstants;
import com.zm.openapi.exception.ValidationException;
import com.zm.openapi.utils.DesUtil;
import com.zm.openapi.utils.SignUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.*;

@Slf4j
@Component
public class OpenapiValidationFilter implements GlobalFilter, Ordered {

    @Value("${api.request.appconfig.cachetime:5}")
    private long appConfigCacheTime; // 限流的时间窗口

    @Autowired
    private RedisTemplate<String, AppSecretBo> appSecretRedisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String uri = serverHttpRequest.getURI().getPath();
        if(!StringUtils.startsWith(uri, "/openapi")) {
            log.error(CommonEnum.ERROR_UNAUTHED_API_PATH.getMsg());
            return Mono.error(new ValidationException(CommonEnum.ERROR_UNAUTHED_API_PATH));
        }

        String appid = serverHttpRequest.getHeaders().getFirst(OpenApiConstants.APP_ID);
        if(StringUtils.isEmpty(appid)) {
            log.error(CommonEnum.ERROR_APPID_NULL.getMsg());
            return Mono.error(new ValidationException(CommonEnum.ERROR_APPID_NULL));
        }

        AppSecretBo secret = loadAppSecret(appid);
        exchange.getAttributes().put(OpenApiConstants.APP_SECRET_OBJECT, secret);
        HttpHeaders headers = serverHttpRequest.getHeaders();

        MediaType contentType = headers.getContentType();
        if(contentType == null) {
            log.error("===============" + JSON.toJSONString(headers));
            log.error("===============" + CommonEnum.ERROR_REQUEST_NONE_CONTENT_TYPE.getMsg());
            return Mono.error(new ValidationException(CommonEnum.ERROR_REQUEST_NONE_CONTENT_TYPE));
        }

        if(!(MediaType.APPLICATION_JSON.equals(contentType)
                || MediaType.APPLICATION_JSON_UTF8.equals(contentType))){
            log.error(contentType.getType() + ":" + contentType.getSubtype());
            log.error(contentType.toString() + ":" + CommonEnum.ERROR_UNSUPPORTED_REQUEST_CONTENT_TYPE.getMsg());
            return Mono.error(new ValidationException(CommonEnum.ERROR_UNSUPPORTED_REQUEST_CONTENT_TYPE));
        }

        String sign = headers.getFirst(OpenApiConstants.SIGN);
        String destext = headers.getFirst(OpenApiConstants.DES_TEXT);
        if(StringUtils.isNotEmpty(destext)) {
            log.debug("处理加密明文");
            // 先验签
            SignUtil.checkSign(secret, destext, sign);

            return playDesTextBody(exchange, chain, secret, destext);
        } else {
            log.debug("无加密明文");
            long contentLength = headers.getContentLength();
            if(contentLength > 0){
                return playPlainTextBody(exchange, chain, secret, sign);
            }
        }

        return chain.filter(exchange);
    }


    private AppSecretBo loadAppSecret(String appid) {
        // 从运营平台拉过来先， 然后放在缓存 。每 10 分钟失效一次。
        AppSecretBo tmp = new AppSecretBo();
        tmp.setAppid(OpenApiConstants.ForTest.appid);
        tmp.setAppsecret(OpenApiConstants.ForTest.appsecret);
        tmp.setDeskey(OpenApiConstants.ForTest.appsecret);
        tmp.setFrequency(OpenApiConstants.ForTest.frequency);
        return tmp;
    }

    @Override
    public int getOrder() {
        return FilterSeq.SEQ_SECOND;
    }


    private Mono<Void> playPlainTextBody(ServerWebExchange exchange, GatewayFilterChain chain, AppSecretBo secret, String sign){
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .flatMap(dataBuffer -> {
                    /*
                     * read the body Flux<DataBuffer>, and release the buffer
                     * //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature
                     * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095
                     */
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                        DataBufferUtils.retain(buffer);
                        return Mono.just(buffer);
                    });

                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };

                    ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();

                    // 原来这个 messageReaders 是设计成成员变量的
                    List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
                    /**
                     * read body string with default messageReaders
                     */
                    return ServerRequest.create(mutatedExchange, messageReaders)
                            .bodyToMono(String.class)
                            .doOnNext(jsonBody -> {
                                play(jsonBody, secret, sign);
                            }).then(chain.filter(mutatedExchange));
                });
    }

    private Mono<Void> playDesTextBody(ServerWebExchange exchange, GatewayFilterChain chain, AppSecretBo secret, String destext){
        // 重构 request 和 body
        ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
            String newBody = counterDes(destext, secret.getDeskey());
            return Mono.just(newBody);
        });
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        //
        headers.remove("Content-Length");
        //
        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
        return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
            ServerHttpRequest decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                public HttpHeaders getHeaders() {
                    long contentLength = headers.getContentLength();
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.putAll(super.getHeaders());
                    if (contentLength > 0L) {
                        httpHeaders.setContentLength(contentLength);
                    } else {
                        httpHeaders.set("Transfer-Encoding", "chunked");
                    }
                    return httpHeaders;
                }
                public Flux<DataBuffer> getBody() {
                    return outputMessage.getBody();
                }
            };

            ServerWebExchange buildedServerWebExchange = exchange.mutate().request(decorator).build();

            return chain.filter(buildedServerWebExchange)
//                    .then(Mono.fromRunnable(()->{
//                        Long startTime = buildedServerWebExchange.getAttribute("startTime");
//                        if (startTime != null){
//                            long executeTime = (System.currentTimeMillis() - startTime);
//                            log.info("耗时：{}ms" , executeTime);
//                            log.info("状态码：{}" , Objects.requireNonNull(buildedServerWebExchange.getResponse().getStatusCode()).value());
//                        }
//                    }))
                    ;
        }));
    }

    private void play(String jsonBody, AppSecretBo secret, String sign) {
        log.debug("[GatewayContext]Read JsonBody:{}", jsonBody);
        SignUtil.checkSign(secret, jsonBody, sign);
    }

    private String counterDes(String desText, String deskey) {
        log.debug("密文{}", desText);
        try{
            return DesUtil.decrypt(desText, deskey);
        }catch (Exception e){
            throw new BusinessException("加密明文解密失败");
        }
    }

}
